﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.TestServer
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.ServiceModel;
    using System.Threading;

    using Phuse.Mime;

    /// <summary>See <see cref="INntpTestServer"/>.</summary>
    [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
    internal class NntpTestServer : INntpTestServer
    {
        private const int ArticleCount = 5000;
        private static readonly object StaticLock = new object();
        private static readonly char[] ArgumentSeparators = new[] { ' ', '\t' };
        private static readonly Random Random = new Random();

        private static Thread serverThread;
        private static Message[] articleList;
        private static int? currentIndex;

        /// <summary>See <see cref="INntpTestServer.Start"/>.</summary>
        public void Start(int port)
        {
            lock (StaticLock)
            {
                if (serverThread != null)
                {
                    throw new InvalidOperationException("The server has already been started.");
                }

                serverThread = new Thread(() => ThreadLoop(port));
                serverThread.Start();
            }
        }

        /// <summary>See <see cref="INntpTestServer.Stop"/>.</summary>
        public void Stop()
        {
            lock (StaticLock)
            {
                if (serverThread == null)
                {
                    throw new InvalidOperationException("The server is not running.");
                }

                serverThread.Abort();
                serverThread.Join();
                serverThread = null;
            }
        }

        [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Console.WriteLine(System.String)", Justification = "Will not be localized.")]
        private static void ThreadLoop(int port)
        {
            try
            {
                TcpListener listener = new TcpListener(IPAddress.Parse("127.0.0.1"), port);
                TcpClient tcpClient;

                Console.WriteLine("NNTP Test Server Started.");

                while (true)
                {
                    try
                    {
                        listener.Start();

                        // Thread.Abort does not seem to work when we're inside an AcceptTcpClient() blocking call ...
                        while (!listener.Pending())
                        {
                            Thread.Sleep(300);
                        }

                        tcpClient = listener.AcceptTcpClient();
                    }
                    finally
                    {
                        listener.Stop();
                    }

                    try
                    {
                        ServiceClient(tcpClient);
                    }
                    catch (IOException)
                    {
                        // Deliberately ignored
                    }
                }
            }
            catch (ThreadAbortException)
            {
            }
            finally
            {
                Console.WriteLine("NNTP Test Server Stopped.");
            }
        }

        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Test Code.")]
        private static void ServiceClient(TcpClient tcpClient)
        {
            lock (StaticLock)
            {
                articleList = null;
            }

            using (tcpClient)
            using (var stream = tcpClient.GetStream())
            using (var reader = new StreamReader(stream, false))
            using (var writer = new StreamWriter(stream) { AutoFlush = true })
            {
                writer.WriteLine("200 Posting allowed");

                while (true)
                {
                    var command = reader.ReadLine().Split(
                        ArgumentSeparators, StringSplitOptions.RemoveEmptyEntries);

                    if (command.Length > 0)
                    {
                        switch (command[0].ToUpperInvariant())
                        {
                            case "MODE":
                                if ((command.Length == 2) && (command[1].ToUpperInvariant() == "READER"))
                                {
                                    writer.WriteLine("200 Posting allowed");
                                    continue;
                                }

                                break;
                            case "GROUP":
                                if (command.Length == 2)
                                {
                                    lock (StaticLock)
                                    {
                                        currentIndex = null;
                                        FillMessages(command[1]);
                                        writer.WriteLine(
                                            "211 {0} {1} {2} {3}",
                                            articleList.Length,
                                            1,
                                            articleList.Length,
                                            command[1]);
                                        continue;
                                    }
                                }

                                break;
                            case "XOVER":
                            case "OVER":
                                if (command.Length == 2)
                                {
                                    var range = command[1].Split('-');
                                    int number;

                                    if ((range.Length == 2) && string.IsNullOrEmpty(range[1]) &&
                                        int.TryParse(range[0], NumberStyles.None, CultureInfo.InvariantCulture, out number) &&
                                        (number > 0))
                                    {
                                        lock (StaticLock)
                                        {
                                            writer.WriteLine("224 Overview information follows");

                                            for (int index = number - 1; index < articleList.Length; ++index)
                                            {
                                                var article = articleList[index];
                                                writer.WriteLine(
                                                    "{0}\t{1}\t\"{2}\" <{3}>\t{4}\t{5}\t{6}\t{7}\t{8}",
                                                    index + 1,
                                                    article.Subject,
                                                    article.From[0].DisplayName,
                                                    article.From[0].EmailAddress,
                                                    ((DateTime)article.Date).ToString("ddd, dd MMM yyyy HH:mm:ss zzz", CultureInfo.InvariantCulture).Remove(29, 1),
                                                    article.MessageId,
                                                    string.Join(" ", article.References == null ? Enumerable.Empty<string>() : article.References),
                                                    0,
                                                    0);
                                            }

                                            writer.WriteLine(".");
                                            continue;
                                        }
                                    }
                                }

                                break;
                            case "NEXT":
                                if (command.Length == 1)
                                {
                                    lock (StaticLock)
                                    {
                                        if (!ArticleSelected(writer))
                                        {
                                            continue;
                                        }

                                        ++currentIndex;

                                        if (currentIndex >= articleList.Length)
                                        {
                                            writer.WriteLine("421 No next article in this group.");
                                            continue;
                                        }

                                        WriteInititalReponseLine((int)currentIndex, writer, 223);
                                        continue;
                                    }
                                }

                                break;
                            case "HEAD":
                                if (command.Length == 1)
                                {
                                    lock (StaticLock)
                                    {
                                        if (!ArticleSelected(writer))
                                        {
                                            continue;
                                        }

                                        WriteInititalReponseLine((int)currentIndex, writer, 221);
                                        articleList[currentIndex.Value].WriteTo(stream);
                                        writer.WriteLine("\r\n.");
                                        continue;
                                    }
                                }

                                break;
                            case "ARTICLE":
                                if (command.Length == 2)
                                {
                                    int number;

                                    if (int.TryParse(command[1], NumberStyles.None, CultureInfo.InvariantCulture, out number))
                                    {
                                        lock (StaticLock)
                                        {
                                            currentIndex = number - 1;

                                            if (!ArticleSelected(writer))
                                            {
                                                continue;
                                            }

                                            WriteInititalReponseLine(currentIndex.Value, writer, 220);
                                            articleList[currentIndex.Value].WriteTo(stream);
                                            writer.WriteLine("\r\n.");
                                            continue;
                                        }
                                    }
                                }

                                break;
                            case "STAT":
                                if (command.Length == 2)
                                {
                                    int number;

                                    if (int.TryParse(command[1], NumberStyles.None, CultureInfo.InvariantCulture, out number))
                                    {
                                        lock (StaticLock)
                                        {
                                            currentIndex = number - 1;

                                            if (!ArticleSelected(writer))
                                            {
                                                continue;
                                            }
                                        }

                                        WriteInititalReponseLine(number - 1, writer, 223);
                                        continue;
                                    }
                                }

                                break;
                            case "QUIT":
                                if (command.Length == 1)
                                {
                                    writer.WriteLine("205 connection closing.");
                                    tcpClient.Client.Shutdown(SocketShutdown.Both);
                                    return;
                                }

                                break;
                        }
                    }

                    writer.WriteLine("501 syntax error in command.");
                }
            }
        }

        private static bool ArticleSelected(StreamWriter writer)
        {
            if (articleList == null)
            {
                writer.WriteLine("412 No newsgroup selected.");
                return false;
            }

            if (!currentIndex.HasValue || (currentIndex.Value >= articleList.Length))
            {
                writer.WriteLine("420 Current article number is invalid.");
                return false;
            }

            return true;
        }

        private static void WriteInititalReponseLine(int index, StreamWriter writer, int responseCode)
        {
            string messageId;

            lock (StaticLock)
            {
                messageId = articleList[index].MessageId;
            }

            writer.WriteLine("{0} {1} {2}", responseCode, index + 1, messageId);
        }

        private static void FillMessages(string groupName)
        {
            articleList = new Message[ArticleCount];
            var body = new TextBody(TextSubtypeNames.Plain)
            {
                Content =
                    "Alice was beginning to get very tired of sitting by her sister on the bank, and of having " +
                    "nothing to do. Once or twice she had peeped into the book her sister was reading, but it " +
                    "had no pictures or conversations in it, \"and what is the use of a book,\" thought Alice, " +
                    "\"without pictures or conversations?\"\r\n\r\n" +
                    "So she was considering in her own mind (as well as she could, for the day made her feel " +
                    "very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the " +
                    "trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes " +
                    "ran close by her."
            };

            for (int index = 0; index < articleList.Length; ++index)
            {
                articleList[index] = new Message()
                {
                    Date = GetDate(index),
                    From = new[] { GetFrom(index) },
                    MessageId = "<" + Guid.NewGuid().ToString("N") + "@phuse.codeplex.com>",
                    References = GetReferences(index),
                    Subject = GetSubject(index),
                    Fields = { new UnstructuredHeaderField("Newsgroups") { Body = groupName } },
                    Body = body
                };
            }
        }

        private static DateTime? GetDate(int index)
        {
            return index == 0 ? DateTime.Now - TimeSpan.FromDays(10) :
                articleList[index - 1].Date + TimeSpan.FromSeconds(Random.Next(300));
        }

        private static Address GetFrom(int index)
        {
            switch (index % 3)
            {
                case 0:
                    return new Address("Mickey Mouse", "mickey@mouse.com");
                case 1:
                    return new Address("Donald Duck", "donald@duck.com");
                default:
                    return new Address("Gladstone Gander", "gladstone@gander.com");
            }
        }

        private static IList<string> GetReferences(int index)
        {
            switch (index % 7)
            {
                case 0:
                case 1:
                case 2:
                    return null;
                default:
                    var referencedMessage = articleList[new Random(index).Next(Math.Max(0, index - 50), index)];
                    var result = new List<string>(
                        referencedMessage.References == null ? new string[0] : referencedMessage.References);
                    result.Add(referencedMessage.MessageId);
                    return result;
            }
        }

        private static string GetSubject(int index)
        {
            switch (index % 7)
            {
                case 0:
                    return "Goofy is Dumb";
                case 1:
                    return "Scrooge has Money";
                case 2:
                    return "Duckville is Boring";
                default:
                    return "Re: " + articleList[new Random(index).Next(Math.Max(0, index - 50), index)].Subject;
            }
        }
    }
}
